// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/common/names.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/js_backend/no_such_method_registry.dart';
import 'package:compiler/src/js_model/elements.dart';
import 'package:compiler/src/js_model/js_world.dart' show JClosedWorld;
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:compiler/src/util/memory_compiler.dart';

class NoSuchMethodInfo {
  final String className;
  final String? superClassName;
  final bool hasThrowingSyntax;
  final bool hasForwardingSyntax;
  final bool isThrowing;
  final bool isDefault;
  final bool isOther;
  final bool isComplexNoReturn;
  final bool isComplexReturn;

  const NoSuchMethodInfo(
    this.className, {
    this.superClassName,
    this.hasThrowingSyntax = false,
    this.hasForwardingSyntax = false,
    this.isThrowing = false,
    this.isDefault = false,
    this.isOther = false,
    this.isComplexNoReturn = false,
    this.isComplexReturn = false,
  });
}

class NoSuchMethodTest {
  final String code;
  final List<NoSuchMethodInfo> methods;
  final bool isNoSuchMethodUsed;

  const NoSuchMethodTest(
    this.code,
    this.methods, {
    this.isNoSuchMethodUsed = false,
  });
}

const List<NoSuchMethodTest> TESTS = const <NoSuchMethodTest>[
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A implements I {
  noSuchMethod(x) => super.noSuchMethod(x);
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A extends B implements I {
  noSuchMethod(x) => super.noSuchMethod(x);
}
class B {}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A extends B implements I {
  noSuchMethod(x) {
    return super.noSuchMethod(x);
  }
}
class B {}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A extends B implements I {
  noSuchMethod(x) => super.noSuchMethod(x);
}
class B {
  noSuchMethod(x) => super.noSuchMethod(x);
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo(
        'A',
        superClassName: 'B',
        hasForwardingSyntax: true,
        isDefault: true,
      ),
      const NoSuchMethodInfo('B', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A extends B implements I {
  noSuchMethod(x) => super.noSuchMethod(x);
}
class B {
  noSuchMethod(x) => throw 'foo';
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo(
        'A',
        superClassName: 'B',
        hasForwardingSyntax: true,
        isThrowing: true,
      ),
      const NoSuchMethodInfo('B', hasThrowingSyntax: true, isThrowing: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
class A {
  noSuchMethod(x) => 3;
}
main() {
  print((new A() as dynamic).foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A implements I {
  noSuchMethod(x, [y]) => super.noSuchMethod(x);
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A implements I {
  noSuchMethod(x, [y]) => super.noSuchMethod(y);
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexNoReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
class A {
  noSuchMethod(x, [y]) => super.noSuchMethod(x) + y;
}
main() {
  print((new A() as dynamic).foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexNoReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
class A {
  noSuchMethod(Invocation x) {
    throw UnsupportedError('');
  }
}
main() {
  print((new A() as dynamic).foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasThrowingSyntax: true, isThrowing: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
class A {
  noSuchMethod(Invocation x) {
    print('foo');
    throw 'foo';
  }
}
main() {
  print((new A() as dynamic).foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexNoReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
class A {
  noSuchMethod(Invocation x) {
    return toString();
  }
}
main() {
  print((new A() as dynamic).foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A implements I {
  noSuchMethod(x) => super.noSuchMethod(x) as dynamic;
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', hasForwardingSyntax: true, isDefault: true),
    ],
  ),
  const NoSuchMethodTest(
    """
abstract class I {
  foo();
}
class A implements I {
  noSuchMethod(x) => super.noSuchMethod(x) as int;
}
main() {
  print(new A().foo());
}
""",
    const <NoSuchMethodInfo>[
      const NoSuchMethodInfo('A', isOther: true, isComplexNoReturn: true),
    ],
    isNoSuchMethodUsed: true,
  ),
];

main() {
  runTests() async {
    for (NoSuchMethodTest test in TESTS) {
      print('---- testing -------------------------------------------------');
      print(test.code);
      CompilationResult result = await runCompiler(
        memorySourceFiles: {'main.dart': test.code},
      );
      Expect.isTrue(result.isSuccess);
      Compiler compiler = result.compiler!;
      checkTest(compiler, test);
    }
  }

  asyncTest(() async {
    print('--test from kernel------------------------------------------------');
    await runTests();
  });
}

checkTest(Compiler compiler, NoSuchMethodTest test) {
  ElementEnvironment frontendEnvironment =
      compiler.frontendStrategy.elementEnvironment;
  NoSuchMethodRegistry registry =
      compiler.frontendStrategy.noSuchMethodRegistry;
  var resolver = registry.internalResolverForTesting;
  final ObjectNSM =
      frontendEnvironment.lookupClassMember(
            compiler.frontendStrategy.commonElements.objectClass,
            Names.noSuchMethod_,
          )
          as FunctionEntity;
  JClosedWorld backendClosedWorld = compiler.backendClosedWorldForTesting!;
  ElementEnvironment backendEnvironment = backendClosedWorld.elementEnvironment;
  NoSuchMethodData data = backendClosedWorld.noSuchMethodData;

  // Test [NoSuchMethodResolver] results for each method.
  for (NoSuchMethodInfo info in test.methods) {
    ClassEntity? cls = frontendEnvironment.lookupClass(
      frontendEnvironment.mainLibrary!,
      info.className,
    );
    Expect.isNotNull(cls, "Class ${info.className} not found.");
    final noSuchMethod =
        frontendEnvironment.lookupClassMember(cls!, Names.noSuchMethod_)
            as FunctionEntity?;
    Expect.isNotNull(noSuchMethod, "noSuchMethod not found in $cls.");

    if (info.superClassName == null) {
      Expect.equals(ObjectNSM, resolver.getSuperNoSuchMethod(noSuchMethod!));
    } else {
      ClassEntity? superclass = frontendEnvironment.lookupClass(
        frontendEnvironment.mainLibrary!,
        info.superClassName!,
      );
      Expect.isNotNull(
        superclass,
        "Superclass ${info.superClassName} not found.",
      );
      final superNoSuchMethod =
          frontendEnvironment.lookupClassMember(
                superclass!,
                Names.noSuchMethod_,
              )
              as FunctionEntity?;
      Expect.isNotNull(
        superNoSuchMethod,
        "noSuchMethod not found in $superclass.",
      );
      Expect.equals(
        superNoSuchMethod,
        resolver.getSuperNoSuchMethod(noSuchMethod!),
        "Unexpected super noSuchMethod for $noSuchMethod.",
      );
    }

    Expect.equals(
      info.hasForwardingSyntax,
      resolver.hasForwardingSyntax(noSuchMethod as JFunction),
      "Unexpected hasForwardSyntax result on $noSuchMethod.",
    );
    Expect.equals(
      info.hasThrowingSyntax,
      resolver.hasThrowingSyntax(noSuchMethod),
      "Unexpected hasThrowingSyntax result on $noSuchMethod.",
    );
  }

  // Test [NoSuchMethodRegistry] results for each method. These are based on
  // the [NoSuchMethodResolver] results which are therefore tested for all
  // methods first.
  for (NoSuchMethodInfo info in test.methods) {
    ClassEntity? frontendClass = frontendEnvironment.lookupClass(
      frontendEnvironment.mainLibrary!,
      info.className,
    );
    Expect.isNotNull(frontendClass, "Class ${info.className} not found.");
    final frontendNoSuchMethod =
        frontendEnvironment.lookupClassMember(
              frontendClass!,
              Names.noSuchMethod_,
            )
            as FunctionEntity?;
    Expect.isNotNull(
      frontendNoSuchMethod,
      "noSuchMethod not found in $frontendClass.",
    );

    Expect.equals(
      info.isDefault,
      registry.defaultImpls.contains(frontendNoSuchMethod),
      "Unexpected isDefault result on $frontendNoSuchMethod.",
    );
    Expect.equals(
      info.isThrowing,
      registry.throwingImpls.contains(frontendNoSuchMethod),
      "Unexpected isThrowing result on $frontendNoSuchMethod.",
    );
    Expect.equals(
      info.isOther,
      registry.otherImpls.contains(frontendNoSuchMethod),
      "Unexpected isOther result on $frontendNoSuchMethod.",
    );

    ClassEntity? backendClass = backendEnvironment.lookupClass(
      backendEnvironment.mainLibrary!,
      info.className,
    );
    Expect.isNotNull(backendClass, "Class ${info.className} not found.");
    final backendNoSuchMethod =
        backendEnvironment.lookupClassMember(backendClass!, Names.noSuchMethod_)
            as FunctionEntity?;
    Expect.isNotNull(
      backendNoSuchMethod,
      "noSuchMethod not found in $backendClass.",
    );

    Expect.equals(
      info.isComplexNoReturn,
      data.complexNoReturnImpls.contains(backendNoSuchMethod),
      "Unexpected isComplexNoReturn result on $backendNoSuchMethod.",
    );
    Expect.equals(
      info.isComplexReturn,
      data.complexReturningImpls.contains(backendNoSuchMethod),
      "Unexpected isComplexReturn result on $backendNoSuchMethod.",
    );
  }

  Expect.equals(
    test.isNoSuchMethodUsed,
    backendClosedWorld.backendUsage.isNoSuchMethodUsed,
    "Unexpected isNoSuchMethodUsed result.",
  );
}
